﻿using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq;
using System.Linq.Expressions;
using Magiq.Providers.Sql.Support;
using MirrorMirror;

namespace Magiq.Providers.Sql.Query
{
    public abstract class EntitySetCompiledQueryableProvider<TExpression> where TExpression : LambdaExpression
    {
        protected static object[] GetParentId(EntitySetAdapter entitySetAdapter)
        {
            var member = entitySetAdapter.Member;
            var id = new object[member.Association.ThisKey.Count];

            for (var i = 0; i < member.Association.ThisKey.Count; i++)
                id[i] = entitySetAdapter.Instance.Get(member.Association.ThisKey[i].Name);

            return id;
        }

        protected static TExpression CreateLambda(EntitySetAdapter entitySetAdapter, LambdaExpression expression)
        {
            var member = entitySetAdapter.Member;
            var elementType = member.Association.OtherType.Type;

            var parentType = member.DeclaringType.Type;

            var dcParameter = Expression.Parameter(typeof(DataContext), "dc");

            var tableExpression = Expression.Call(dcParameter, "GetTable", new[] { parentType });

            var getByIdparameter = Expression.Parameter(parentType, "x");
            var idValueparameter = Expression.Parameter(typeof(object[]), "id");

            Expression getByByBody = null;

            for (var i = 0; i < member.Association.ThisKey.Count; i++)
            {
                var parentKeyName = member.Association.ThisKey[i].Name;
                Expression parentKeyValueExpression = Expression.ArrayIndex(idValueparameter, Expression.Constant(i));

                if (member.Association.ThisKey[i].Type != typeof(object))
                    parentKeyValueExpression = Expression.Convert(parentKeyValueExpression,
                                                                  member.Association.ThisKey[i].Type);

                var e = Expression.Equal(Expression.Property(getByIdparameter, parentKeyName),
                                         parentKeyValueExpression);
                getByByBody = getByByBody == null ? e : Expression.AndAlso(getByByBody, e);
            }

            var lambdaGetById = Expression.Lambda(getByByBody, getByIdparameter);

            var getById = Expression.Call(typeof(Queryable),
                                          "Where",
                                          new[] { parentType },
                                          tableExpression,
                                          Expression.Quote(lambdaGetById));

            var parameter = Expression.Parameter(parentType, "x");
            Expression body = Expression.Property(parameter, member.Name);

            var collectionType = typeof(IEnumerable<>).MakeGenericType(elementType);
            if (member.Type != collectionType)
                body = Expression.Convert(body, collectionType);

            var lambdaDelegateType = typeof(Func<,>).MakeGenericType(parentType, collectionType);

            var lambda = Expression.Lambda(lambdaDelegateType, body, parameter);

            var childrenExpression = Expression.Call(typeof(Queryable),
                                                     "SelectMany",
                                                     new[] { parentType, elementType },
                                                     getById,
                                                     Expression.Quote(lambda));

            var resultExpression =
                new ExpressionParameterShifter(expression.Parameters[0], childrenExpression).Visit(expression.Body);

            var argsParameters = new ParameterExpression[expression.Parameters.Count + 1];
            argsParameters[0] = dcParameter;
            argsParameters[1] = idValueparameter;
            for (var i = 1; i < expression.Parameters.Count; i++)
                argsParameters[i + 1] = expression.Parameters[i];

            var delegateType = typeof(TExpression).GetGenericArguments().Single();
            return (TExpression)Expression.Lambda(delegateType, resultExpression, argsParameters);
        }

    }

    public class EntitySetEntitySetCompiledQueryableProvider<T, TResult> :
        EntitySetCompiledQueryableProvider<Expression<Func<DataContext, object[], IQueryable<TResult>>>> where T : class
    {
        private static readonly IDictionary<string, Func<DataContext, object[], IQueryable<TResult>>> CompiledQueries =
            new Dictionary<string, Func<DataContext, object[], IQueryable<TResult>>>();

        public static IQueryable<TResult> GetQueryable(EntitySetAdapter entitySetAdapter, string key,
                                                       Expression<Func<IQueryable<T>, IQueryable<TResult>>> expression)
        {
            var dc = entitySetAdapter.DataContext;
            Func<DataContext, object[], IQueryable<TResult>> compiledQuery;

            if (!CompiledQueries.TryGetValue(key, out compiledQuery))
            {
                var lambda = CreateLambda(entitySetAdapter, expression);
                compiledQuery = CompiledQuery.Compile(lambda);
                CompiledQueries.Add(key, compiledQuery);
            }

            return compiledQuery(dc, GetParentId(entitySetAdapter));
        }
    }

    public class EntitySetEntitySetCompiledQueryableProvider<T, TArg0, TResult> :
        EntitySetCompiledQueryableProvider<Expression<Func<DataContext, object[], TArg0, IQueryable<TResult>>>> where T : class
    {
        private static readonly IDictionary<string, Func<DataContext, object[], TArg0, IQueryable<TResult>>>
            CompiledQueries = new Dictionary<string, Func<DataContext, object[], TArg0, IQueryable<TResult>>>();

        public static IQueryable<TResult> GetQueryable(EntitySetAdapter entitySetAdapter, string key,
                                                       Expression<Func<IQueryable<T>, TArg0, IQueryable<TResult>>>
                                                           expression, TArg0 arg0)
        {
            Func<DataContext, object[], TArg0, IQueryable<TResult>> compiledQuery;

            if (!CompiledQueries.TryGetValue(key, out compiledQuery))
            {
                var lambda = CreateLambda(entitySetAdapter, expression);
                compiledQuery = CompiledQuery.Compile(lambda);
                CompiledQueries.Add(key, compiledQuery);
            }

            var dc = entitySetAdapter.DataContext;

            return compiledQuery(dc, GetParentId(entitySetAdapter), arg0);
        }
    }

    public class EntitySetEntitySetCompiledQueryableProvider<T, TArg0, TArg1, TResult> :
        EntitySetCompiledQueryableProvider<Expression<Func<DataContext, object[], TArg0, TArg1, IQueryable<TResult>>>>
        where T : class
    {
        private static readonly IDictionary<string, Func<DataContext, object[], TArg0, TArg1, IQueryable<TResult>>>
            CompiledQueries = new Dictionary<string, Func<DataContext, object[], TArg0, TArg1, IQueryable<TResult>>>();

        public static IQueryable<TResult> GetQueryable(EntitySetAdapter entitySetAdapter, string key,
                                                       Expression
                                                           <Func<IQueryable<T>, TArg0, TArg1, IQueryable<TResult>>>
                                                           expression, TArg0 arg0, TArg1 arg1)
        {
            Func<DataContext, object[], TArg0, TArg1, IQueryable<TResult>> compiledQuery;

            if (!CompiledQueries.TryGetValue(key, out compiledQuery))
            {
                var lambda = CreateLambda(entitySetAdapter, expression);
                compiledQuery = CompiledQuery.Compile(lambda);
                CompiledQueries.Add(key, compiledQuery);
            }

            var dc = entitySetAdapter.DataContext;

            return compiledQuery(dc, GetParentId(entitySetAdapter), arg0, arg1);
        }
    }
}